﻿using System.Linq.Expressions;
using System.Reflection.Metadata;

namespace K9Nano.Extensions;

public static class ExpressionExtensions
{
    public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> leftExpression,
       Expression<Func<T, bool>> rightExpression) =>
       Combine(leftExpression, rightExpression, Expression.AndAlso);

    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> leftExpression,
        Expression<Func<T, bool>> rightExpression) =>
        Combine(leftExpression, rightExpression, Expression.Or);

    public static Expression<Func<T, bool>> OrElse<T>(this Expression<Func<T, bool>> leftExpression, Expression<Func<T, bool>> rightExpression)
        => Combine(leftExpression, rightExpression, Expression.OrElse);

    public static Expression<Func<T, bool>> Combine<T>(Expression<Func<T, bool>> leftExpression, Expression<Func<T, bool>> rightExpression, Func<Expression, Expression, BinaryExpression> combineOperator)
    {
        var leftParameter = leftExpression.Parameters[0];
        var rightParameter = rightExpression.Parameters[0];

        var visitor = new ReplaceParameterVisitor(rightParameter, leftParameter);

        var leftBody = leftExpression.Body;
        var rightBody = visitor.Visit(rightExpression.Body);

        return Expression.Lambda<Func<T, bool>>(combineOperator(leftBody, rightBody), leftParameter);
    }

    public static Expression<Func<T, bool>> AndAlsoIf<T>(this Expression<Func<T, bool>> leftExpression,
        bool contidion,
       Expression<Func<T, bool>> rightExpression) =>
      contidion ? Combine(leftExpression, rightExpression, Expression.AndAlso) : leftExpression;

    public static IQueryable<T> Expect<T>(this IQueryable<T> query, string propertyName, object? expected)
    {
        return query.Where(BuildEqual<T>(propertyName, expected));
    }

    public static Expression<Func<T, bool>> BuildEqual<T>(string propertyName, object? expected)
    {
        var parameter = Expression.Parameter(typeof(T), "x");
        var property = typeof(T).GetProperty(propertyName)
            ?? throw new InvalidOperationException($"{typeof(T)} has no property named {propertyName}");

        var propertyType = property.PropertyType;

        ConstantExpression valueExpression;
        if (expected == null && property.PropertyType.IsValueType)
        {
            valueExpression = Expression.Constant(null, typeof(Nullable<>).MakeGenericType(property.PropertyType));
        }
        else
        {
            valueExpression = Expression.Constant(expected, propertyType);
        }

        var propertyExpression = Expression.Property(parameter, property);  // x.<propertyName>

        return Expression.Lambda<Func<T, bool>>(
                Expression.Equal(
                    propertyExpression,
                    valueExpression
                ),
                parameter
            );
    }

    private class ReplaceParameterVisitor(ParameterExpression oldParameter, ParameterExpression newParameter) : ExpressionVisitor
    {
        protected override Expression VisitParameter(ParameterExpression node)
        {
            return ReferenceEquals(node, oldParameter) ? newParameter : base.VisitParameter(node);
        }
    }
}
